// Copyright (C) 2004, 2008 International Business Machines and others.
// All Rights Reserved.
// This code is published under the Eclipse Public License.
//
// Authors:  Carl Laird, Andreas Waechter     IBM    2004-08-13

#include "IpoptConfig.h"
#include "IpOptionsList.hpp"

#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>

namespace Ipopt
{

bool OptionsList::SetStringValue(
   const std::string& tag,
   const std::string& value,
   bool               allow_clobber, /* = true */
   bool               dont_print     /* = false */
)
{
   if( IsValid(reg_options_) )
   {
      SmartPtr<const RegisteredOption> option = reg_options_->GetOption(tag);

      if( IsNull(option) )
      {
         if( IsValid(jnlst_) )
         {
            jnlst_->Printf(J_ERROR, J_MAIN,
                           "Tried to set Option: %s. It is not a valid option. Please check the list of available options.\n", tag.c_str());
         }
         //THROW_EXCEPTION(OPTION_INVALID, msg);
         return false;
      }

      if( option->Type() != OT_String )
      {
         if( IsValid(jnlst_) )
         {
            std::string msg = "Tried to set Option: " + tag;
            msg += ". It is a valid option, but it is of type ";
            if( option->Type() == OT_Number )
            {
               msg += " Number";
            }
            else if( option->Type() == OT_Integer )
            {
               msg += " Integer";
            }
            else
            {
               msg += " Unknown";
            }
            msg += ", not of type String. Please check the documentation for options.\n";
            jnlst_->Printf(J_ERROR, J_MAIN, "%s", msg.c_str());
            option->OutputDescription(*jnlst_);
         }
         //THROW_EXCEPTION(OPTION_INVALID, msg);
         return false;
      }

      if( !option->IsValidStringSetting(value) )
      {
         if( IsValid(jnlst_) )
         {
            jnlst_->Printf(J_ERROR, J_MAIN,
                           "Setting: \"%s\" is not a valid setting for Option: %s. Check the option documentation.\n",
                           value.c_str(), tag.c_str());
            option->OutputDescription(*jnlst_);
         }
         //THROW_EXCEPTION(OPTION_INVALID, msg);
         return false;
      }
   }

   if( !will_allow_clobber(tag) )
   {
      if( IsValid(jnlst_) )
      {
         std::string msg = "WARNING: Tried to set option \"" + tag;
         msg += "\" to a value of \"" + value;
         msg += "\",\n         but the previous value is set to disallow clobbering.\n";
         msg += "         The setting will remain as: \"" + tag;
         msg += " " + options_[lowercase(tag)].GetValue();
         msg += "\"\n";
         jnlst_->Printf(J_WARNING, J_MAIN, "%s", msg.c_str());
      }
   }
   else
   {
      //    if (will_allow_clobber(tag)) {
      OptionsList::OptionValue optval(value, allow_clobber, dont_print);
      options_[lowercase(tag)] = optval;
   }
   return true;

   //     std::string msg = "Option: \"" + tag;
   //     msg += " ";
   //     msg += value;
   //     msg += "\" not taken because a value of \n\"" ;
   //     msg += options_[lowercase(tag)].GetValue();
   //     msg += "\" already exists and is set to disallow clobbering.\n\n";
   //     jnlst_->Printf(J_ERROR, J_MAIN, "%s", msg.c_str());
   //     return false;
}

bool OptionsList::SetNumericValue(
   const std::string& tag,
   Number             value,
   bool               allow_clobber, /* = true */
   bool               dont_print     /* = false */
)
{
   char buffer[256];
   Snprintf(buffer, 255, "%g", value);

   if( IsValid(reg_options_) )
   {
      SmartPtr<const RegisteredOption> option = reg_options_->GetOption(tag);

      if( IsNull(option) )
      {
         if( IsValid(jnlst_) )
         {
            jnlst_->Printf(J_ERROR, J_MAIN,
                           "Tried to set Option: %s. It is not a valid option. Please check the list of available options.\n", tag.c_str());
         }
         //THROW_EXCEPTION(OPTION_INVALID, msg);
         return false;
      }

      if( option->Type() != OT_Number )
      {
         if( IsValid(jnlst_) )
         {
            std::string msg = "Tried to set Option: " + tag;
            msg += ". It is a valid option, but it is of type ";
            if( option->Type() == OT_String )
            {
               msg += " String";
            }
            else if( option->Type() == OT_Integer )
            {
               msg += " Integer";
            }
            else
            {
               msg += " Unknown";
            }
            msg += ", not of type Number. Please check the documentation for options.\n";
            jnlst_->Printf(J_ERROR, J_MAIN, "%s", msg.c_str());
            option->OutputDescription(*jnlst_);
         }
         //THROW_EXCEPTION(OPTION_INVALID, msg);
         return false;
      }

      if( !option->IsValidNumberSetting(value) )
      {
         if( IsValid(jnlst_) )
         {
            jnlst_->Printf(J_ERROR, J_MAIN,
                           "Setting: \"%s\" is not a valid setting for Option: %s. Check the option documentation.\n",
                           buffer, tag.c_str());
            option->OutputDescription(*jnlst_);
         }
         //THROW_EXCEPTION(OPTION_INVALID, msg);
         return false;
      }
   }

   if( !will_allow_clobber(tag) )
   {
      if( IsValid(jnlst_) )
      {
         std::string msg = "WARNING: Tried to set option \"" + tag;
         msg += "\" to a value of \"";
         msg += buffer;
         msg += "\",\n         but the previous value is set to disallow clobbering.\n";
         msg += "         The setting will remain as: \"" + tag;
         msg += " " + options_[lowercase(tag)].GetValue();
         msg += "\"\n";
         jnlst_->Printf(J_WARNING, J_MAIN, "%s", msg.c_str());
      }
   }
   else
   {
      OptionsList::OptionValue optval(buffer, allow_clobber, dont_print);
      options_[lowercase(tag)] = optval;
   }
   return true;
}

bool OptionsList::SetIntegerValue(
   const std::string& tag,
   Index              value,
   bool               allow_clobber, /* = true */
   bool               dont_print     /* = false */
)
{
   char buffer[256];
   Snprintf(buffer, 255, "%" IPOPT_INDEX_FORMAT, value);

   if( IsValid(reg_options_) )
   {
      SmartPtr<const RegisteredOption> option = reg_options_->GetOption(tag);

      if( IsNull(option) )
      {
         if( IsValid(jnlst_) )
         {
            jnlst_->Printf(J_ERROR, J_MAIN,
                           "Tried to set Option: %s. It is not a valid option. Please check the list of available options.\n", tag.c_str());
         }
         //THROW_EXCEPTION(OPTION_INVALID, msg);
         return false;
      }

      if( option->Type() != OT_Integer )
      {
         if( IsValid(jnlst_) )
         {
            std::string msg = "Tried to set Option: " + tag;
            msg += ". It is a valid option, but it is of type ";
            if( option->Type() == OT_String )
            {
               msg += " String";
            }
            else if( option->Type() == OT_Number )
            {
               msg += " Number";
            }
            else
            {
               msg += " Unknown";
            }
            msg += ", not of type Integer. Please check the documentation for options.\n";
            jnlst_->Printf(J_ERROR, J_MAIN, "%s", msg.c_str());
            option->OutputDescription(*jnlst_);
         }
         //THROW_EXCEPTION(OPTION_INVALID, msg);
         return false;
      }

      if( !option->IsValidIntegerSetting(value) )
      {
         if( IsValid(jnlst_) )
         {
            jnlst_->Printf(J_ERROR, J_MAIN,
                           "Setting: \"%s\" is not a valid setting for Option: %s. Check the option documentation.\n",
                           buffer, tag.c_str());
            option->OutputDescription(*jnlst_);
         }
         //THROW_EXCEPTION(OPTION_INVALID, msg);
         return false;
      }
   }

   if( !will_allow_clobber(tag) )
   {
      if( IsValid(jnlst_) )
      {
         std::string msg = "WARNING: Tried to set option \"" + tag;
         msg += "\" to a value of \"";
         msg += buffer;
         msg += "\",\n         but the previous value is set to disallow clobbering.\n";
         msg += "         The setting will remain as: \"" + tag;
         msg += " " + options_[lowercase(tag)].GetValue();
         msg += "\"\n";
         jnlst_->Printf(J_WARNING, J_MAIN, "%s", msg.c_str());
      }
   }
   else
   {
      //    if (will_allow_clobber(tag)) {
      OptionsList::OptionValue optval(buffer, allow_clobber, dont_print);
      options_[lowercase(tag)] = optval;
   }
   return true;
}

bool OptionsList::UnsetValue(
   const std::string& tag
)
{
   if( !will_allow_clobber(tag) )
   {
      if( IsValid(jnlst_) )
      {
         std::string msg = "WARNING: Tried to unset option \"" + tag;
         msg += "\",\n         but the current value is set to disallow clobbering.\n";
         msg += "         The setting will remain as: \"" + tag;
         msg += " " + options_[lowercase(tag)].GetValue();
         msg += "\"\n";
         jnlst_->Printf(J_WARNING, J_MAIN, "%s", msg.c_str());
      }
      return false;
   }

   return options_.erase(lowercase(tag)) > 0;
}

bool OptionsList::SetStringValueIfUnset(
   const std::string& tag,
   const std::string& value,
   bool               allow_clobber, /* = true */
   bool               dont_print     /* = false */
)
{
   std::string val;
   bool found = GetStringValue(tag, val, "");
   if( !found )
   {
      return SetStringValue(tag, value, allow_clobber, dont_print);
   }
   return true;
}

bool OptionsList::SetNumericValueIfUnset(
   const std::string& tag,
   Number             value,
   bool               allow_clobber, /* = true */
   bool               dont_print     /* = false */
)
{
   Number val;
   bool found = GetNumericValue(tag, val, "");
   if( !found )
   {
      return SetNumericValue(tag, value, allow_clobber, dont_print);
   }
   return true;
}

bool OptionsList::SetIntegerValueIfUnset(
   const std::string& tag,
   Index              value,
   bool               allow_clobber, /* = true */
   bool               dont_print     /* = false */
)
{
   Index val;
   bool found = GetIntegerValue(tag, val, "");
   if( !found )
   {
      return SetIntegerValue(tag, value, allow_clobber, dont_print);
   }
   return true;
}

bool OptionsList::GetStringValue(
   const std::string& tag,
   std::string&       value,
   const std::string& prefix
) const
{
   SmartPtr<const RegisteredOption> option = NULL;

   bool found = find_tag(tag, prefix, value);

   if( IsValid(reg_options_) )
   {
      option = reg_options_->GetOption(tag);
      if( IsNull(option) )
      {
         std::string msg = "IPOPT tried to get the value of Option: " + tag;
         msg += ". It is not a valid registered option.";
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }

      if( option->Type() != OT_String )
      {
         std::string msg = "IPOPT tried to get the value of Option: " + tag;
         msg += ". It is a valid option, but it is of type ";
         if( option->Type() == OT_Integer )
         {
            msg += " Integer";
         }
         else if( option->Type() == OT_Number )
         {
            msg += " Number";
         }
         else
         {
            msg += " Unknown";
         }
         msg += ", not of type String. Please check the documentation for options.";
         if( IsValid(jnlst_) )
         {
            option->OutputDescription(*jnlst_);
         }
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }

      if( found )
      {
         value = option->MapStringSetting(value);
      }
      else
      {
         value = option->DefaultString();
      }
   }

   return found;
}

bool OptionsList::GetEnumValue(
   const std::string& tag,
   Index&             value,
   const std::string& prefix
) const
{
   std::string str;
   SmartPtr<const RegisteredOption> option = NULL;

   bool found = find_tag(tag, prefix, str);

   if( IsValid(reg_options_) )
   {
      option = reg_options_->GetOption(tag);
      if( IsNull(option) )
      {
         std::string msg = "IPOPT tried to get the value of Option: " + tag;
         msg += ". It is not a valid registered option.";
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }

      if( option->Type() != OT_String )
      {
         std::string msg = "IPOPT tried to get the value of Option: " + tag;
         msg += ". It is a valid option, but it is of type ";
         if( option->Type() == OT_Integer )
         {
            msg += " Integer";
         }
         else if( option->Type() == OT_Number )
         {
            msg += " Number";
         }
         else
         {
            msg += " Unknown";
         }
         msg += ", not of type String. Please check the documentation for options.";
         if( IsValid(jnlst_) )
         {
            option->OutputDescription(*jnlst_);
         }
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }

      if( found )
      {
         value = option->MapStringSettingToEnum(str);
      }
      else
      {
         value = option->DefaultStringAsEnum();
      }
   }

   return found;
}

bool OptionsList::GetBoolValue(
   const std::string& tag,
   bool&              value,
   const std::string& prefix
) const
{
   std::string str;
   bool ret = GetStringValue(tag, str, prefix);
   if( str == "no" || str == "false" || str == "off" )
   {
      value = false;
   }
   else if( str == "yes" || str == "true" || str == "on" )
   {
      value = true;
   }
   else
   {
      THROW_EXCEPTION(OPTION_INVALID, "Tried to get a boolean from an option and failed.");
   }

   return ret;
}

bool OptionsList::GetNumericValue(
   const std::string& tag,
   Number&            value,
   const std::string& prefix
) const
{
   SmartPtr<const RegisteredOption> option = NULL;

   if( IsValid(reg_options_) )
   {
      option = reg_options_->GetOption(tag);
      if( IsNull(option) )
      {
         std::string msg = "IPOPT tried to get the value of Option: " + tag;
         msg += ". It is not a valid registered option.";
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }

      if( option->Type() != OT_Number )
      {
         std::string msg = "IPOPT tried to get the value of Option: " + tag;
         msg += ". It is a valid option, but it is of type ";
         if( option->Type() == OT_Integer )
         {
            msg += " Integer";
         }
         else if( option->Type() == OT_String )
         {
            msg += " String";
         }
         else
         {
            msg += " Unknown";
         }
         msg += ", not of type Number. Please check the documentation for options.";
         if( IsValid(jnlst_) )
         {
            option->OutputDescription(*jnlst_);
         }
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }
   }

   std::string strvalue;
   if( find_tag(tag, prefix, strvalue) )
   {
      // Some people like to use 'd' instead of 'e' in floating point
      // numbers.  Therefore, we change a 'd' to an 'e'
      char* buffer = new char[strvalue.length() + 1];
      strcpy(buffer, strvalue.c_str());
      for( size_t i = 0; i < strvalue.length(); ++i )
      {
         if( buffer[i] == 'd' || buffer[i] == 'D' )
         {
            buffer[i] = 'e';
         }
      }
      char* p_end;
      Number retval = strtod(buffer, &p_end);
      if( *p_end != '\0' && !isspace(*p_end) )
      {
         delete[] buffer;
         std::string msg = "Option \"" + tag + "\": Double value expected, but non-numeric value \"" + strvalue
                           + "\" found.\n";
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }
      delete[] buffer;
      value = retval;
      return true;
   }
   else if( IsValid(option) )
   {
      value = option->DefaultNumber();
      return false;
   }
   return false;
}

bool OptionsList::GetIntegerValue(
   const std::string& tag,
   Index&             value,
   const std::string& prefix
) const
{
   SmartPtr<const RegisteredOption> option = NULL;

   if( IsValid(reg_options_) )
   {
      option = reg_options_->GetOption(tag);
      if( IsNull(option) )
      {
         std::string msg = "IPOPT tried to get the value of Option: " + tag;
         msg += ". It is not a valid registered option.";
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }

      if( option->Type() != OT_Integer )
      {
         std::string msg = "IPOPT tried to get the value of Option: " + tag;
         msg += ". It is a valid option, but it is of type ";
         if( option->Type() == OT_Number )
         {
            msg += " Number";
         }
         else if( option->Type() == OT_String )
         {
            msg += " String";
         }
         else
         {
            msg += " Unknown";
         }
         msg += ", not of type Integer. Please check the documentation for options.";
         if( IsValid(jnlst_) )
         {
            option->OutputDescription(*jnlst_);
         }
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }
   }

   std::string strvalue;
   if( find_tag(tag, prefix, strvalue) )
   {
      char* p_end;
      size_t retval = strtol(strvalue.c_str(), &p_end, 10);
      if( *p_end != '\0' && !isspace(*p_end) )
      {
         std::string msg = "Option \"" + tag + "\": Integer value expected, but non-integer value \"" + strvalue
                           + "\" found.\n";
         THROW_EXCEPTION(OPTION_INVALID, msg);
      }
      value = static_cast<Index>(retval);
      return true;
   }
   else if( IsValid(option) )
   {
      value = option->DefaultInteger();
      return false;
   }

   return false;
}

const std::string& OptionsList::lowercase(
   const std::string& tag
) const
{
   lowercase_buffer_ = tag;
   for( Index i = 0; i < (Index) tag.length(); i++ )
   {
      lowercase_buffer_[i] = (char) tolower(tag[i]);
   }
   return lowercase_buffer_;
}

void OptionsList::PrintList(
   std::string& list
) const
{
   list.erase();
   char buffer[256];
   Snprintf(buffer, 255, "%40s   %-20s %s\n", "Name", "Value", "# times used");
   list += buffer;
   for( std::map<std::string, OptionValue>::const_iterator p = options_.begin(); p != options_.end(); ++p )
   {
      Snprintf(buffer, 255, "%40s = %-20s %6" IPOPT_INDEX_FORMAT "\n", p->first.c_str(), p->second.Value().c_str(), p->second.Counter());
      list += buffer;
   }
}

void OptionsList::PrintUserOptions(
   std::string& list
) const
{
   list.erase();
   char buffer[256];
   Snprintf(buffer, 255, "%40s   %-20s %s\n", "Name", "Value", "used");
   list += buffer;
   for( std::map<std::string, OptionValue>::const_iterator p = options_.begin(); p != options_.end(); ++p )
   {
      if( !p->second.DontPrint() )
      {
         const char yes[] = "yes";
         const char no[] = "no";
         const char* used;
         if( p->second.Counter() > 0 )
         {
            used = yes;
         }
         else
         {
            used = no;
         }
         Snprintf(buffer, 255, "%40s = %-20s %4s\n", p->first.c_str(), p->second.Value().c_str(), used);
         list += buffer;
      }
   }
}

bool OptionsList::ReadFromStream(
   const Journalist& jnlst,
   std::istream&     is,
   bool              allow_clobber
)
{
   jnlst.Printf(J_DETAILED, J_MAIN,
                "Start reading options from stream.\n");

   while( true )
   {
      std::string tag;
      std::string value;

      if( !readnexttoken(is, tag) )
      {
         // That's it - end of file reached.
         jnlst.Printf(J_DETAILED, J_MAIN,
                      "Finished reading options from file.\n");
         return true;
      }

      if( !readnexttoken(is, value) )
      {
         // Can't read value for a given tag
         jnlst.Printf(J_ERROR, J_MAIN,
                      "Error reading value for tag %s from file.\n", tag.c_str());
         return false;
      }

      // Now add the value for the options list
      jnlst.Printf(J_DETAILED, J_MAIN,
                   "Adding option \"%s\" with value \"%s\" to OptionsList.\n", tag.c_str(), value.c_str());

      if( IsValid(reg_options_) )
      {
         SmartPtr<const RegisteredOption> option = reg_options_->GetOption(tag);
         if( IsNull(option) )
         {
            std::string msg = "Read Option: \"";
            msg += tag;
            msg += "\". It is not a valid option. Check the list of available options.";
            THROW_EXCEPTION(OPTION_INVALID, msg);
         }

         if( option->Type() == OT_String )
         {
            bool result = SetStringValue(tag, value, allow_clobber);
            ASSERT_EXCEPTION(result, OPTION_INVALID,
                             "Error setting string value read from option file.");
         }
         else if( option->Type() == OT_Number )
         {
            // Some people like to use 'd' instead of 'e' in floating
            // point numbers.  Therefore, we change a 'd' to an 'e'
            char* buffer = new char[value.length() + 1];
            strcpy(buffer, value.c_str());
            for( size_t i = 0; i < value.length(); ++i )
            {
               if( buffer[i] == 'd' || buffer[i] == 'D' )
               {
                  buffer[i] = 'e';
               }
            }
            char* p_end;
            Number retval = strtod(buffer, &p_end);
            if( *p_end != '\0' && !isspace(*p_end) )
            {
               delete[] buffer;
               std::string msg = "Option \"" + tag + "\": Double value expected, but non-numeric option value \""
                                 + value + "\" found.\n";
               THROW_EXCEPTION(OPTION_INVALID, msg);
            }
            delete [] buffer;
            bool result = SetNumericValue(tag, retval, allow_clobber);
            ASSERT_EXCEPTION(result, OPTION_INVALID,
                             "Error setting numeric value read from file.");
         }
         else if( option->Type() == OT_Integer )
         {
            char* p_end;
            size_t retval = strtol(value.c_str(), &p_end, 10);
            if( *p_end != '\0' && !isspace(*p_end) )
            {
               std::string msg = "Option \"" + tag + "\": Integer value expected, but non-integer option value \""
                                 + value + "\" found.\n";
               if( IsValid(jnlst_) )
               {
                  option->OutputDescription(*jnlst_);
               }
               THROW_EXCEPTION(OPTION_INVALID, msg);
            }
            bool result = SetIntegerValue(tag, static_cast<Index>(retval), allow_clobber);
            ASSERT_EXCEPTION(result, OPTION_INVALID,
                             "Error setting integer value read from option file.");
         }
         else
         {
            DBG_ASSERT(false && "Option Type: Unknown");
         }
      }
      else
      {
         bool result = SetStringValue(tag, value, allow_clobber);
         ASSERT_EXCEPTION(result, OPTION_INVALID,
                          "Error setting value read from option file.");
      }
   }
}

bool OptionsList::find_tag(
   const std::string& tag,
   const std::string& prefix,
   std::string&       value
) const
{
   bool found = false;
   std::map<std::string, OptionValue>::const_iterator p;

   if( prefix != "" )
   {
      p = options_.find(lowercase(prefix + tag));
      if( p != options_.end() )
      {
         found = true;
      }
   }

   if( !found )
   {
      p = options_.find(lowercase(tag));
      if( p != options_.end() )
      {
         found = true;
      }
   }

   if( found )
   {
      value = p->second.GetValue();
   }

   return found;
}

bool OptionsList::will_allow_clobber(
   const std::string& tag
) const
{
   bool allow_clobber = true;
   std::map<std::string, OptionValue>::const_iterator p;

   p = options_.find(lowercase(tag));
   if( p != options_.end() )
   {
      allow_clobber = p->second.AllowClobber();
   }

   return allow_clobber;
}

bool OptionsList::readnexttoken(
   std::istream& is,
   std::string&  token
)
{
   token.erase();
   char c = is.get();

   // First get rid of all comments and white spaces
   while( !is.eof() && (isspace(c) || c == '#') )
   {
      if( c == '#' )
      {
         is.ignore(10000000, '\n');
      }
      c = is.get();
   }

   bool inside_quotes = (c == '"');
   if( inside_quotes )
   {
      if( is.eof() )
      {
         return false;   // eof after quotation symbol
      }
      c = is.get();
   }

   if( is.eof() )
   {
      return false;
   }

   // Now read the token
   while( !is.eof() && (inside_quotes || !isspace(c)) )
   {
      token += (char) c;
      c = is.get();
      if( inside_quotes && (c == '"') )
      {
         inside_quotes = false;
         if( !is.eof() )
         {
            c = is.get();
         }
      }
   }

   return !inside_quotes;
}

} // namespace Ipopt
